use crate::config::UploadConfig;
use axum::{
    async_trait,
    body::{Body, Bytes},
    extract::{DefaultBodyLimit, FromRequestParts, Multipart, Path, Query, State},
    http::{header, request::Parts, Request, StatusCode},
    response::{Html, IntoResponse, Json, Response},
    routing::post,
    BoxError, Router,
};
use chrono::Utc;
use futures::{Stream, TryStreamExt};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json;
use std::{io, vec};
use tokio::{fs::File, io::BufWriter};
use tokio_util::io::StreamReader;
use tower_http::services::ServeDir;
use uuid;
mod config;

struct ContentLength(Option<u64>);

#[async_trait]
impl<S> FromRequestParts<S> for ContentLength
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, &'static str);

    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
        if let Some(user_agent) = parts.headers.get(header::CONTENT_LENGTH) {
            user_agent
                .to_str()
                .map_err(|_| (StatusCode::BAD_REQUEST, "`Content-length` value is error"))?
                .parse()
                .map_err(|_| (StatusCode::BAD_REQUEST, "`Content-length` value is error"))
                .map(|s| Self(Some(s)))
        } else {
            Ok(Self(None))
        }
    }
}

static FILENAME_STRIP_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r#"[/\\:*?"<>|\t\n\r\f]"#).unwrap());

fn empty_string_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    if s.is_empty() {
        Ok(None)
    } else {
        Ok(Some(s))
    }
}

fn default_id() -> String {
    "id".to_string()
}

#[derive(Debug, Deserialize)]
struct UploadParams {
    #[serde(deserialize_with = "empty_string_is_none", default)]
    callback: Option<String>,
    ie: Option<String>,
    #[serde(default = "default_id")]
    id: String,
}

impl UploadParams {
    fn is_ie(&self) -> bool {
        self.ie.is_some() && self.ie != Some("false".to_string())
    }
}

#[derive(Debug, Serialize)]
struct File2 {
    id: String,
    name: String,
    size: u64,
    url: String,
    hash: Option<String>,
}

async fn single_upload_request_body(
    State(conf): State<UploadConfig>,
    Query(params): Query<UploadParams>,
    Path(file_name): Path<String>,
    content_length: ContentLength,
    request: Request<Body>,
) -> Result<Response, (StatusCode, String)> {
    conf.check_body_length(content_length.0)?;
    let stm = request.into_body().into_data_stream();
    let files: Vec<File2> = vec![stream_to_file(&file_name, stm, &conf).await?];
    return Ok(wrap_files(&params, files));
}

async fn single_upload_form(
    State(conf): State<UploadConfig>,
    Query(params): Query<UploadParams>,
    content_length: ContentLength,
    mut multipart: Multipart,
) -> Result<Response, (StatusCode, String)> {
    conf.check_body_length(content_length.0)?;
    let mut files: Vec<File2> = vec![];
    while let Some(field) = multipart.next_field().await.unwrap() {
        let file_name = if let Some(file_name) = field.file_name() {
            file_name.to_owned()
        } else {
            continue;
        };
        let file = stream_to_file(&file_name, field, &conf).await?;
        files.push(file);
    }
    return Ok(wrap_files(&params, files));
}

fn wrap_files(params: &UploadParams, files: Vec<File2>) -> Response {
    let mut response = if let Some(callback) = &params.callback {
        let files = if files.len() == 1 {
            serde_json::to_string(&files.get(0).unwrap()).unwrap()
        } else {
            serde_json::to_string(&files).unwrap()
        };
        Html(
            format!("<script type='text/javascript' type='language'>var a= parent?parent.window:window;a.{}('{}','{}',0);</script>",callback,files,params.id)
        ).into_response()
    } else {
        if files.len() == 1 {
            Json(files.get(0).unwrap()).into_response()
        } else {
            Json(files).into_response()
        }
    };
    if params.is_ie() {
        response.headers_mut().insert(
            header::CONTENT_TYPE,
            "text/html; Charset=utf-8".parse().unwrap(),
        );
    }
    response
}

async fn stream_to_file<S, E>(
    filename: &str,
    stream: S,
    conf: &UploadConfig,
) -> Result<File2, (StatusCode, String)>
where
    S: Stream<Item = Result<Bytes, E>>,
    E: Into<BoxError>,
{
    let filename = secure_filename(filename);

    async {
        // stream.next().await;
        // Convert the stream into an `AsyncRead`.
        let body_with_io_error = stream.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
        let body_reader = StreamReader::new(body_with_io_error);
        futures::pin_mut!(body_reader);
        // Create the file. `File` implements `AsyncWrite`.

        let buffer = &mut uuid::Uuid::encode_buffer();
        let uid = uuid::Uuid::new_v4().as_simple().encode_lower(buffer);
        let date = Utc::now().format(&conf.date).to_string();
        let date_id = std::path::Path::new(&date).join(&uid);
        let folder = std::path::Path::new(&conf.root).join(&date_id);
        let url = std::path::Path::new(&conf.url_prefix)
            .join(&conf.raw_prefix)
            .join(&date_id)
            .join(&filename);

        tokio::fs::create_dir_all(&folder).await.unwrap();

        let path = folder.join(&filename);

        let mut file = BufWriter::new(File::create(&path).await?);

        // Copy the body into the file.
        if let Err(err) = tokio::io::copy(&mut body_reader, &mut file).await {
            tokio::fs::remove_file(path).await.unwrap();
            tokio::fs::remove_dir_all(folder).await.unwrap();
            return Err(err);
        };
        let size = tokio::fs::metadata(path).await.unwrap().len();
        Ok::<_, io::Error>(File2 {
            id: date_id.to_str().unwrap().to_string(),
            name: filename,
            size,
            url: url.to_str().unwrap().to_string(),
            hash: None,
        })
    }
    .await
    .map_err(|err| {
        println!("{:?}", err);
        match err.into_inner() {
            Some(e) => {
                return (StatusCode::PAYLOAD_TOO_LARGE, e.to_string());
            }
            _ => (StatusCode::INTERNAL_SERVER_ERROR, "err".to_string()),
        }
    })
}

fn secure_filename(filename: &str) -> String {
    FILENAME_STRIP_RE.replace_all(filename, "_").to_string()
}

pub fn get_route<S>(cfg: Option<UploadConfig>) -> Router<S>
where
    S: Sync + Send + Clone + 'static,
{
    let conf = cfg.unwrap_or(UploadConfig::from_env());
    let serve_dir_from_dist = ServeDir::new(&conf.root);
    Router::new()
        .layer(DefaultBodyLimit::max(
            conf.max_body_length.try_into().unwrap(),
        ))
        .route(
            &format!("/{}/:file_name", conf.upload_prefix),
            post(single_upload_request_body),
        )
        .route(
            &format!("/{}", conf.upload_prefix),
            post(single_upload_form),
        )
        .nest_service(&format!("/{}", conf.raw_prefix), serve_dir_from_dist)
        .with_state(conf)
}
